package org.vt.ece3574.pictoslider;

import java.io.EOFException;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.StreamCorruptedException;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;

/**
 * This class is used for saving, reading, and removing high scores from
 * internal storage.
 * 
 * @author Michael
 */
public class HighScoreManager {

	// File names
	/**
	 * File where the high score times are stored.
	 */
	private static final String TIME_FILE_BASE = "HighScoresFile";
	/**
	 * File where the number of moves for a high score are stored.
	 */
	private static final String MOVES_FILE_BASE = "MovesFile";
	/**
	 * Base file name for where the high score images are stored. File names
	 * follow the format BASE + i, where i is a number corresponding to that
	 * particular high score.
	 */
	private static final String PIC_FILE_BASE = "ImageFile";
	/**
	 * String which is added to the File bases when the file stores high scores
	 * based on the fewest amount of moves.
	 */
	private static final String BY_MOVES = "Moves";

	/**
	 * Reference to the application's context, needed to store and read from
	 * internal storage.
	 */
	private Context context;

	/**
	 * This enum represents one of 4 Difficulties, from EASY to CRAZY. Each
	 * difficulty has a String as well as a couple of Resource Id's associated
	 * with it. These values can be obtained by methods within the Difficulty
	 * enum.
	 * 
	 * @author Michael
	 */
	public enum Difficulty {
		EASY("Easy"), MEDIUM("Medium"), HARD("Hard"), CRAZY("Crazy");

		/**
		 * String representation of the Difficulty
		 */
		private final String asString;

		/**
		 * Private constructor for the Difficulty enum.
		 * 
		 * @param s
		 *            - String Representation for the Difficulty.
		 */
		private Difficulty(String s) {
			asString = s;
		}

		/**
		 * @return A String representation of the Difficulty
		 */
		public String asString() {
			return asString;
		}

		/**
		 * @return The Resource Id of the TableLayout within this specific
		 *         Difficulty's Tab on the High Score Screen.
		 */
		public int id() {
			switch (this) {
			case EASY:
				return R.id.tableLayoutEasy;
			case MEDIUM:
				return R.id.tableLayoutMedium;
			case HARD:
				return R.id.tableLayoutHard;
			case CRAZY:
				return R.id.tableLayoutCrazy;
			}
			return R.id.tableLayoutEasy;
		}

		/**
		 * @return The Resource Id of the FrameLayout within this specific
		 *         Difficulty's Tab on the High Score Screen.
		 */
		public int frameId() {
			switch (this) {
			case EASY:
				return R.id.frameLayoutEasy;
			case MEDIUM:
				return R.id.frameLayoutMedium;
			case HARD:
				return R.id.frameLayoutHard;
			case CRAZY:
				return R.id.frameLayoutCrazy;
			}
			return R.id.tableLayoutEasy;
		}
	}

	/**
	 * Only constructor for the class.
	 * 
	 * @param c
	 *            - The application's context.
	 */
	public HighScoreManager(Context c) {
		context = c;
	}

	/**
	 * Deletes all high scores from internal storage.
	 */
	public void removeAllHighScores() {
		for (Difficulty d : Difficulty.values()) {
			removeHighScoresForDifficulty(d);
		}
	}

	/**
	 * Remove all high scores with the Difficulty specified by d.
	 * 
	 * @param d
	 *            - The Difficulty.
	 */
	private void removeHighScoresForDifficulty(Difficulty d) {
		context.deleteFile(TIME_FILE_BASE + d.asString());
		context.deleteFile(MOVES_FILE_BASE + d.asString());
		for (int i = 0; i < 10; i++) {
			context.deleteFile(PIC_FILE_BASE + d.asString() + i);
		}
		context.deleteFile(TIME_FILE_BASE + BY_MOVES + d.asString());
		context.deleteFile(MOVES_FILE_BASE + BY_MOVES + d.asString());
		for (int i = 0; i < 10; i++) {
			context.deleteFile(PIC_FILE_BASE + BY_MOVES + d.asString() + i);
		}
	}

	/**
	 * Adds the new high score to internal storage if it is in the top 10 (by
	 * time or by moves, or both) in its difficulty.
	 * 
	 * @param moves
	 *            - The number of moves it took to solve a puzzle.
	 * @param time
	 *            - The time it took to solve a puzzle.
	 * @param image
	 *            - The puzzle's image.
	 * @param d
	 *            - The Difficulty of the puzzle solved.
	 */
	public void update(int moves, long time, Bitmap image, Difficulty d) {
		updateBy(false, moves, time, image, d);
		updateBy(true, moves, time, image, d);
	}

	/**
	 * Adds the new high score to internal storage if it is in the top 10 (by
	 * time or by moves, but not both) in its difficulty.
	 * 
	 * @param byMoves
	 *            - True if the high score should be judged by number of moves,
	 *            false if it should be judged by fastest time.
	 * @param moves
	 *            - Number of moves used to solve the puzzle.
	 * @param time
	 *            - Length of time it took to solve the puzzle.
	 * @param image
	 *            - Picture for the puzzle.
	 * @param d
	 *            - Difficulty of the puzzle.
	 */
	private void updateBy(boolean byMoves, int moves, long time, Bitmap image,
			Difficulty d) {
		// Get all of the old high scores, so they can be compared to the new
		// score.
		List<HighScoreObject> highScores = getHighScoreListForDifficulty(d,
				byMoves);

		// Will set to true if the score needs to be added.
		boolean addScore = false;

		// Loop through the list if it has 10 or more elements, otherwise just
		// add the new high score.
		if (!(highScores.isEmpty())) {
			if (highScores.size() >= 10) {
				for (HighScoreObject hs : highScores) {

					boolean isBetter = byMoves ? (hs.getMoves() > moves) : (hs
							.getTime() > time);

					// If this score is better than the old one, swap the two
					// scores and images so that the new score is in the high
					// scores, and the old one is being checked to see if it is
					// good enough to stay.
					if (isBetter) {
						addScore = true;
						long tempLong = time;
						Bitmap tempBitmap = image;
						int tempInt = moves;
						time = hs.getTime();
						image = hs.getImage();
						moves = hs.getMoves();
						hs.setTime(tempLong);
						hs.setImage(tempBitmap);
						hs.setMoves(tempInt);
					}
				}
			} else {
				highScores.add(new HighScoreObject(moves, time, image));
				addScore = true;
			}
		} else {
			highScores.add(new HighScoreObject(moves, time, image));
			addScore = true;
		}

		// Put the high scores in order.
		Collections.sort(highScores);

		// If the score must be added, add it.
		if (addScore) {
			ObjectOutputStream scoreOos = null;
			ObjectOutputStream movesOos = null;
			FileOutputStream picOs = null;
			try {

				// Filename is different from the base if by Moves
				String byMovesFile = byMoves ? (BY_MOVES) : ("");

				// Open the score file from internal storage.
				scoreOos = new ObjectOutputStream(context.openFileOutput(
						TIME_FILE_BASE + byMovesFile + d.asString(),
						Context.MODE_PRIVATE));

				// Open the moves file
				movesOos = new ObjectOutputStream(context.openFileOutput(
						MOVES_FILE_BASE + byMovesFile + d.asString(),
						Context.MODE_PRIVATE));

				// For each high score, open the picture file from internal
				// storage, store the score, and store the picture.
				for (int i = 0; i < 10; i++) {
					picOs = context.openFileOutput(PIC_FILE_BASE + byMovesFile
							+ d.asString() + i, Context.MODE_PRIVATE);
					HighScoreObject hso = null;

					// Prevent OutOfBounds exception from occuring
					if (i < highScores.size()) {
						hso = highScores.get(i);
					}
					// If there is no high score at this index, put -1, as the
					// high score reader knows this means there is no high score
					// at this index. Otherwise store the score and image.
					if (hso == null) {
						scoreOos.writeLong(-1);
						movesOos.writeInt(-1);
					} else {
						scoreOos.writeLong(hso.getTime());
						movesOos.writeInt(hso.getMoves());
						hso.getImage().compress(Bitmap.CompressFormat.PNG, 100,
								picOs);
					}
				}
				// Flush and close the output streams.
				scoreOos.flush();
				movesOos.flush();
				picOs.flush();
				scoreOos.close();
				movesOos.close();
				picOs.close();
			} catch (FileNotFoundException e) {
				e.printStackTrace();
			} catch (IOException e) {
				e.printStackTrace();
			}
		}
	}

	/**
	 * Retrieves a list of HighScoreObjects belonging to the specified
	 * difficulty from internal storage. Does not run in its own thread, so do
	 * not use this function too liberally.
	 * 
	 * @param d
	 *            - The difficulty of high scores to return.
	 * @param byMoves
	 *            - Return top 10 by Moves if true, by Time if false.
	 * 
	 * @return A List of HighScoreObjects containing all of the high scores.
	 */
	public List<HighScoreObject> getHighScoreListForDifficulty(Difficulty d,
			boolean byMoves) {

		// Filename is different from the Base if by Moves
		String byMovesFile = byMoves ? (BY_MOVES) : ("");

		List<HighScoreObject> hsList = new LinkedList<HighScoreObject>();
		ObjectInputStream scoreOis = null;
		ObjectInputStream movesOis = null;
		FileInputStream picIs = null;
		try {
			// Open the file with the scores.
			scoreOis = new ObjectInputStream(
					context.openFileInput(TIME_FILE_BASE + byMovesFile
							+ d.asString()));
			movesOis = new ObjectInputStream(
					context.openFileInput(MOVES_FILE_BASE + byMovesFile
							+ d.asString()));
			int count = 0;
			// Loop until the end of the file
			while (true) {
				// Open the file with the picture for this score.
				picIs = context.openFileInput(PIC_FILE_BASE + byMovesFile
						+ d.asString() + count);
				count++;
				long tempScore = scoreOis.readLong();
				int tempMoves = movesOis.readInt();
				Bitmap tempImage = BitmapFactory.decodeStream(picIs);

				// Check that this is a real high score before adding it.
				if (tempScore != -1) {
					HighScoreObject hsObject = new HighScoreObject(tempMoves,
							tempScore, tempImage);
					hsObject.sortByMoves(byMoves);
					hsList.add(hsObject);
				}
			}
		} catch (EOFException e) {
			// Do Nothing. This catch block executes when the while loop above
			// is terminated.
			try {
				scoreOis.close();
				movesOis.close();
				picIs.close();
			} catch (IOException e1) {
				e1.printStackTrace();
			}
		} catch (StreamCorruptedException e) {
			e.printStackTrace();
		} catch (FileNotFoundException e) {
			// Do Nothing, File will be created
		} catch (IOException e) {
			e.printStackTrace();
		}
		return hsList;
	}

}
